<template>
  <ElDialog
    class="fa-image-cropper__dialog"
    :visible.sync="visible"
    :width="`${dialogWidth}px`"
    top="0"
    :title="title"
    :show-close="closeable"
    append-to-body
    :close-on-click-modal="false"
    @closed="$emit('close')"
  >
    <div
      class="fa-image-cropper bg-texture"
      :style="`width: ${container.width}px; height: ${container.height}px;`"
      v-loading="loading || submitLoading"
      @mousedown.left.prevent="drag.dragStart"
      @touchstart="drag.dragStart"
      @mousewheel.prevent="onWheel"
    >
      <canvas ref="canvas" class="fa-image-cropper__canvas" :width="crop.width" :height="crop.height"></canvas>

      <img v-if="url" ref="img" :src="url" class="fa-image-cropper__origin" :style="proxyStyle" @load="imgLoaded" />

      <div
        class="fa-image-cropper__area"
        :class="{ round }"
        :style="`width: ${area.width}px; height: ${area.height}px;`"
      >
        <div
          class="fa-image-cropper__fake-ct bg-texture"
          :style="`width: ${container.width}px; height: ${container.height}px; transform: translate(-${space.x}px, -${space.y}px)`"
        >
          <img v-if="url" :src="url" class="fa-image-cropper__fake" :style="proxyStyle" />
        </div>
      </div>
    </div>

    <template #footer>
      <div class="fa-image-cropper__footer-btns">
        <ElButton @click="reset">重置</ElButton>
        <ElButton type="primary" :loading="submitLoading" :disabled="loading" @click="confirm">裁剪</ElButton>
      </div>
      <div class="fa-image-cropper__footer-tips">通过鼠标拖拽和滚轮缩放控制图片</div>
    </template>
  </ElDialog>
</template>

<script lang="ts">
import { computed, defineComponent, PropType, ref, toRefs } from 'vue'
import useImageUrl from './useImageUrl'
import useCropperState from './useCropperState'
import { useDrag, base64ToFile } from '@/utils'
import { isIE } from './utils'

export default defineComponent({
  name: 'FaImageCropper',

  props: {
    /** 图片源地址*/
    src: {
      type: String,
      default: ''
    },

    /** 图片源文件 */
    file: {
      type: File
    },

    /** 弹框标题 */
    title: {
      type: String,
      default: '图片裁剪'
    },

    /** 图片格式，'png' | 'jpg' | 'jpeg' */
    format: {
      type: String,
      default: 'png'
    },

    /** 裁剪尺寸 */
    size: {
      type: String,
      default: '500,500'
    },

    /** 容器尺寸 */
    containerSize: {
      type: String,
      default: '300,300'
    },

    /** 裁剪区域是否为圆形 */
    round: {
      type: Boolean,
      default: false
    },

    /** 是否可关闭 (关闭按钮是否显示) */
    closeable: {
      type: Boolean,
      default: true
    },

    /** 提交回调 */
    submit: {
      type: Function as PropType<(image: string, file: File) => void>
    }
  },

  setup(props, { emit }) {
    /** 是否可见 */
    const visible = ref(true)

    /** 画板 */
    const canvas = ref<null | HTMLCanvasElement>(null)

    /** 图片节点 */
    const img = ref<null | HTMLImageElement>(null)

    /** 状态 */
    const state = useCropperState(props)

    /** 提交 loading */
    const submitLoading = ref(false)

    /** 调整位置边界 */
    function adjustRange({ left, top }: { left: number; top: number }) {
      const { maxTop, minTop, maxLeft, minLeft } = state.range

      if (top > maxTop) {
        top = maxTop
      } else if (top < minTop) {
        top = minTop
      }

      if (left > maxLeft) {
        left = maxLeft
      } else if (left < minLeft) {
        left = minLeft
      }

      return { left, top }
    }

    /** 代理图片样式 */
    const proxyStyle = computed(() => {
      const { width, height, top, left } = state.proxy
      return `width: ${width}px; height: ${height}px; transform: translate(${left}px, ${top}px)`
    })

    /** 重置图片位置尺寸 */
    function reset() {
      const {
        container: { width: cw, height: ch },
        natural: { width: nw, height: nh },
        initScale: scale,
        proxy
      } = state
      const width = nw * scale
      const height = nh * scale

      /** 代理图片尺寸定位 */
      Object.assign(proxy, {
        width,
        height,
        top: (ch - nh * scale) / 2,
        left: (cw - nw * scale) / 2,
        scale
      })
    }

    /** 获取截取后的图片 base64 */
    function getImageBase64() {
      const cv = canvas.value
      const ctx = cv?.getContext('2d')

      if (!cv || !ctx) return ''

      const { area, crop, space, proxy } = state
      const { scale } = proxy
      const { format } = props

      let sx = -(proxy.left - space.x) / scale,
        sy = -(proxy.top - space.y) / scale
      const sWidth = area.width / scale,
        sHeight = area.height / scale,
        width = crop.width,
        height = crop.height,
        x = 0,
        y = 0

      /** 清理 canvas */
      ctx.clearRect(0, 0, width, height)

      /** jpg 背景填白 */
      if (['jpeg', 'jpg'].includes(format)) {
        ctx.fillStyle = '#fff'
        ctx.fillRect(0, 0, width, height)
      }

      /** IE 不支持负值 */
      if (isIE) {
        sx = Math.max(sx, 0)
        sy = Math.max(sy, 0)
      }

      /** 绘制裁剪区域图像 */
      ctx.drawImage(img.value as HTMLImageElement, sx, sy, sWidth, sHeight, x, y, width, height)

      /** 获取图像数据 */
      return cv.toDataURL('image/' + format)
    }

    return {
      /** 是否可见 */
      visible,

      /** 画板 */
      canvas,

      /** 图片 */
      img,

      /** 提交 loading */
      submitLoading,

      /** 图片地址 */
      ...useImageUrl(props),

      ...toRefs(state),

      /** 代理图片样式 */
      proxyStyle,

      /** 图片加载就绪 */
      imgLoaded(e: Event) {
        const { naturalWidth: width, naturalHeight: height } = e.target as HTMLImageElement

        Object.assign(state.natural, { width, height })

        reset()
      },

      /** 重置图片位置尺寸 */
      reset,

      /** 调整位置边界 */
      adjustRange,

      /** 拖拽位置 */
      drag: useDrag<{ left: number; top: number }>({
        /** 移动开始 */
        onStart(e) {
          const { left, top } = state.proxy
          return { left, top }
        },

        /** 移动中 */
        onMoving(e, { x: moveX, y: moveY }, { left: startLeft, top: startTop }) {
          Object.assign(
            state.proxy,
            adjustRange({
              left: startLeft + moveX,
              top: startTop + moveY
            })
          )
        }
      }),

      /** 滚轮缩放 */
      onWheel(e: WheelEvent & { wheelDelta: number }) {
        const { proxy, natural, o, initScale } = state
        let scale = proxy.scale / initScale // 相对初始缩放的缩放比
        const _width = proxy.width
        const scaleChange = 0.05

        scale += (e.deltaY || -e.wheelDelta) < 0 ? scaleChange : -scaleChange

        if (isIE && scale < 1) scale = 1

        if (scale < 0.2) scale = 0.2

        const proxyScale = (proxy.scale = scale * initScale)

        proxy.width = natural.width * proxyScale
        proxy.height = natural.height * proxyScale

        const _scale = proxy.width / _width

        Object.assign(
          proxy,
          adjustRange({
            left: (proxy.left - o.left) * _scale + o.left,
            top: (proxy.top - o.top) * _scale + o.top
          })
        )
      },

      /** 裁剪图片 */
      async confirm() {
        const image = getImageBase64()
        const file = base64ToFile(image, props.file?.name)

        emit('success', image)

        submitLoading.value = true

        try {
          await props.submit?.(image, file)

          visible.value = false
        } finally {
          submitLoading.value = false
        }
      }
    }
  }
})
</script>

<style lang="scss">
.fa-image-cropper {
  position: relative;
  overflow: hidden;
  cursor: grab;
  user-select: none;

  // 对话框
  &__dialog {
    .el-dialog {
      position: absolute;
      top: 30%;
      left: 50%;
      transform: translate(-50%, -30%);

      &__header {
        padding: 10px 16px;
      }

      &__headerbtn {
        top: 12px;
        right: 16px;
      }

      &__body {
        padding: 8px 16px;
      }

      &__footer {
        padding: 8px 0 16px;
        text-align: center;
      }
    }

    // 透明背景纹理
    .bg-texture {
      $c: rgba(0, 0, 0, 0.25);
      $s: 20px;
      background-image: linear-gradient(45deg, $c 25%, transparent 0, transparent 75%, $c 0),
        linear-gradient(45deg, $c 25%, transparent 0, transparent 75%, $c 0);
      background-position: 0 0, calc($s / 2) calc($s / 2);
      background-size: $s $s;
    }
  }

  &::after {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 9;
    background: rgba(0, 0, 0, 0.5);
    box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.4);
    content: '';
  }

  &__origin,
  &__fake,
  &__fake-ct {
    position: absolute;
    top: 0;
    left: 0;
  }

  &__origin,
  &__fake {
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
  }

  &__canvas {
    position: absolute;
    top: 100%;
    left: 0;
    visibility: hidden;
  }

  &__area {
    position: absolute;
    top: 50%;
    left: 50%;
    z-index: 10;
    overflow: hidden;
    background-color: #fff;
    transform: translate(-50%, -50%);

    // 圆形
    &.round {
      border-radius: 50%;
    }
  }

  &__footer {
    &-tips {
      margin-top: 8px;
      color: $color-info;
      font-size: 12px;
    }
  }
}
</style>
